#!/usr/bin/ruby

APP_NAME = "smslog2ical.rb"
APP_VERSION = "0.1"

# smslog2ical.rb
#
# An somewhat obnoxious, but effective script that will copy your iPhone's
# SMS messages into a calendar you specify.
#
# It reads the existing SMS message backup and only enters events that are new.
#
# KNOWN ISSUES:
# * The ical entry will only contain the name of the contact that called IF the
#		contact exists in your iPHONE's address book, not your Mac's address book.
# * Only works in Leopard.

# "smslog2ical.rb" is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as published 
# by the Free Software Foundation.
# 
# "smslog2ical.rb" is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with "smslog2ical.rb"; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA	 02110-1301	 USA

require 'optparse'

class Numeric
	
	def to_s_as_time
		 NSDate.dateWithTimeIntervalSince1970(self).description
	end
	
end

excludeList = []
excludeIncompleteCalls = false
$verboseMessages = false
oparse = OptionParser.new {|op|
	op.banner = "Usage : smslog2ical.rb [options] [CALENDARNAME]"
	op.on("-v","--verbose","Run verbosely") {
		$verboseMessages = true
	}
	op.on("-e NUMBER","--exclude-number NUMBER","Exclude SMS from the given number") { |number|
		excludeList << number.delete("^0123456789+")
	}
	op.on("-x EXCLUDE-FILE","--exclude-file EXCLUDE-FILE",
	"Exclude calls listed in EXCLUDE-FILE.",
	"EXCLUDE-FILE should contain a whitespace-",
	"separated list of telephone numbers."
	) { |file|
		File.open(file,"r") do |fp|
			fp.read.split(/\W/).each {|number|
				excludeList << number.delete("^0123456789+")
			}
		end
	}
	op.on_tail("-h","-?","Show this message.") {
		puts op
		exit(0)
	}
}

destinationCalendarName = oparse.parse!(ARGV)[0] || "SMS Log"

require 'rubygems'
require 'osx/cocoa'
require 'sqlite3'

include OSX

def verbose(str)
	NSLog(str) if $verboseMessages
end

excludeList.each {|n| verbose("Excluding number: #{n}")}

verbose("Loading CalendarStore library")
NSBundle.bundleWithPath("/System/Library/Frameworks/CalendarStore.framework").load
ns_import :CalendarStore

class CLGSMS < OSX::NSObject

	attr_accessor :date, :phoneNumber, :callerID, :contactDB, :message, :flags
	
		def contact
			retVal = @phoneNumber || ""
			if @contactDB && @phoneNumber.length > 0
				if @callerID == -1
					 @contactDB.execute("SELECT value, record_id FROM ABMultiValue WHERE 1") do |contact|
						 if contact[0] and contact[0].gsub(/[^\d+]/, '') == @phoneNumber
							 @callerID = contact[1]
						 end
					 end
				 end
				 begin
					 @contactDB.execute("SELECT `First`,`Last`,`Organization` FROM ABPerson WHERE ROWID = #{@callerID}") do |row|
							retVal = if row[0] != nil && row[0] != ''
								row[0] + " " + row[1]
							else
								row[2]
							end
					 end
				 rescue Exception => e
					 verbose("An error ocurred when trying to look up a name for the number : #{@phoneNumber}")
					 verbose(e.message)
					 retVal = @phoneNumber  || ""
				 end
			end
			
			return retVal.strip
		end
		
		def isOutgoing
			flags == 3 ? true : false
		end
		
		def inspect
			"#<CLGCall:"+ ("0x%x" % [object_id]) + " "+ \
			([:date, :phoneNumber,
				:callerID,:message,:flags].collect {|sym| sym.to_s + "=" + method(sym).call.inspect}).join(" ") + ">"
		end
end

class NSDate
	def inspect
		return description.to_s.inspect
	end
end


sms = []
IPHONE_SMS_LOG_PATH = NSString.stringWithString("Library/SMS/sms.db")
IPHONE_AB_PATH = NSString.stringWithString("Library/AddressBook/AddressBook.sqlitedb")

verbose("Finding calendar named #{destinationCalendarName}")
@selectedCalendar = CalCalendarStore.defaultCalendarStore().calendars.detect{|cal| cal.title == destinationCalendarName}

unless @selectedCalendar
	verbose("The calendar #{destinationCalendarName} does not exist.")
	exit(1) 
end

# load data from cellphone backup store

dirs = Dir.glob(NSHomeDirectory() + "/Library/Application Support/MobileSync/Backup/*")
#find phone name
phoneDir = dirs[0] # we could enumerate thru each to get every iPhone

unless phoneDir
	verbose(<<-MSG 
	No files were found in /Library/Application Support/MobileSync/Backup/.
	This folder is supposed to contain backup data from your iPhone, but was found to be empty.
	Sync your iphone and run #{APP_NAME} again.
MSG
	)
	exit(1)
end

class DeviceInfoUnavailableError < RuntimeError; end

@phoneName = "Unknown Device"
begin
  verbose("Reading #{phoneDir}/Info.plist for Phone Name")
  raise DeviceInfoUnavailableError unless FileTest.file? phoneDir + "/Info.plist"
  
  infoData = NSData.dataWithContentsOfFile_(phoneDir + "/Info.plist")
  raise DeviceInfoUnavailableError unless infoData
  
  infoPlist = NSPropertyListSerialization.propertyListFromData_mutabilityOption_format_errorDescription_(
    infoData, NSPropertyListImmutable, nil, nil)
  raise DeviceInfoUnavailableError unless infoPlist
  
  @phoneName = infoPlist.objectForKey_("Device Name") 
  verbose("Phone name is #{@phoneName}")
#  end
rescue DeviceInfoUnavailableError
  Nlog("The Info.plist describing device could not be found. " + 
  "Your device's name will not appear in recorded entries.")
end

smsSQLData,abSQLData = nil, nil
Dir.glob(phoneDir + "/*.mdbackup") do |testPath|
	#one of these files contains the phone log
	thisData = NSData.dataWithContentsOfFile_(testPath)
	thisPlist = NSPropertyListSerialization.propertyListFromData_mutabilityOption_format_errorDescription_(
		thisData, NSPropertyListImmutable, nil, nil)
	if (thisPlist.objectForKey_("Path") == IPHONE_SMS_LOG_PATH)
		verbose("Found iPhone SMS Log db file #{File.basename(testPath)}")
		smsSQLData = NSData.dataWithData(thisPlist.objectForKey_("Data"))
	elsif (thisPlist.objectForKey_("Path") == IPHONE_AB_PATH)
		verbose("Found iPhone Address Book db file #{File.basename(testPath)}")
		abSQLData = NSData.dataWithData(thisPlist.objectForKey_("Data"))
	end
end

if (smsSQLData && abSQLData) then
	verbose("Address book and SMS Log both found")
	Dir.mkdir(NSHomeDirectory() + "/Library/Caches/iPhoneSMSLog/") unless
		FileTest.exist?(NSHomeDirectory() + "/Library/Caches/iPhoneSMSLog/")
	
	verbose("Extracting sqlite backup data")
	smsSQLData.writeToFile_atomically_(NSHomeDirectory() + "/Library/Caches/iPhoneSMSLog/sms_db.sqlite3", 
	true)
	
	abSQLData.writeToFile_atomically_(NSHomeDirectory() + "/Library/Caches/iPhoneSMSLog/addressbook_db.sqlite3", 
	true)
	verbose("Backup data extracted")
		
	verbose("Opening extracted databases")
	smsdb = SQLite3::Database.new(NSHomeDirectory() + "/Library/Caches/iPhoneSMSLog/sms_db.sqlite3")
	contactdb = SQLite3::Database.new(NSHomeDirectory() + "/Library/Caches/iPhoneSMSLog/addressbook_db.sqlite3")
	
	verbose("Determining range of events in SMS log")
	firstBUSMSDate, lastBUSMSDate = smsdb.execute("SELECT MIN(date),MAX(date) FROM message WHERE 1")[0].collect{|v|v.to_i}
	verbose("First event in backup: #{firstBUSMSDate.to_s_as_time}")
	verbose("Last event in backup : #{lastBUSMSDate.to_s_as_time}")
	
	verbose("Discovering events already in Calendar Store.	Calendar name: #{@selectedCalendar.title}")
	eventPred = CalCalendarStore.eventPredicateWithStartDate_endDate_calendars_(
	NSDate.dateWithTimeIntervalSince1970(firstBUSMSDate),NSDate.date,[@selectedCalendar])
	events = CalCalendarStore.defaultCalendarStore().eventsWithPredicate(eventPred)
	verbose("Currently #{events.size} events in calendar at given range")
	lastEvent = (events.sort{|a,b| a.startDate.compare(b.startDate)}).last
	verbose("Last event in calendar store: #{lastEvent.inspect}")
	
	lastEventTime = (lastEvent != nil) ? lastEvent.startDate.timeIntervalSince1970 : 0
	verbose("Will read events from backup ocurring after #{lastEventTime.to_s_as_time}")

	verbose("Reading call log for new events to add")
	smsdb.execute("SELECT * FROM message WHERE date > '#{lastEventTime}'") do |row|
		newSMS = CLGSMS.alloc.init
		newSMS.date = NSDate.dateWithTimeIntervalSince1970(row[2])
		newSMS.phoneNumber = row[1]
		newSMS.contactDB = contactdb
		newSMS.callerID = -1
		newSMS.flags = row[4].to_i
		newSMS.message = row[3].to_s
		sms << newSMS
	end
	smsdb.close
end
verbose("Done reading events to add, total events to add: #{sms.size}")

sms.each do |sm|
	next if (excludeList.include? sm.phoneNumber)
	newEvent = CalEvent.alloc.init
	contactString = sm.contact
	smsDirection = sm.isOutgoing ? "to" : "from"
	smsName = (contactString && contactString.length > 0) ? sm.contact : "unidentifed caller"
	newEvent.setTitle("SMS #{smsDirection} #{sm.contact}: #{sm.message}")
	newEvent.setStartDate(sm.date)
	newEvent.setEndDate(sm.date)
	newEvent.setCalendar(@selectedCalendar)
	verbose("Inserting CalEvent for SMS #{sm.inspect}")
	CalCalendarStore.defaultCalendarStore().saveEvent_span_error_(newEvent, :CalSpanThisEvent, nil)
end
contactdb.close
verbose("Finished adding events to calendar")

verbose("Throwing away temp files")
File.unlink(NSHomeDirectory() + "/Library/Caches/iPhoneSMSLog/sms_db.sqlite3")
File.unlink(NSHomeDirectory() + "/Library/Caches/iPhoneSMSLog/addressbook_db.sqlite3")

verbose("exiting now")
exit(0)
